using System;
using System.Linq;

namespace MirrorMirror
{
    public static class ReflectionExtensions
    {
        private static readonly ReflectionBuilder Builder = new ReflectionBuilder();

        private static readonly DelegatesCache Cache = new DelegatesCache();

        private static string Key(Type type, string member)
        {
            return type.FullName + "." + member;
        }

        private static object ExecuteChain(object obj, string chain, Func<object, string, object> lastChain)
        {
            var value = obj;
            var members = chain.Split('.');

            for (var i = 0; i < members.Length - 1; i++)
                value = ExecuteGet(value, members[i]);

            return lastChain(value, members.Last());
        }

        private static object ExecuteGet(object obj, string fieldOrProperty)
        {
            var getter = GetOrPut(obj.GetType(), fieldOrProperty, () => Builder.FieldOrPropertyGetter(obj.GetType(), fieldOrProperty));
            return getter(obj);
        }

        public static object Get(this object self, string chain)
        {
            return ExecuteChain(self, chain, ExecuteGet);
        }
        
        public static object Invoke(this object self, string chain)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object>>(x, m)(self));
        }

        public static object Invoke(this object self, string chain, object arg1)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object>>(x, m,arg1)(x,arg1.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object>>(x, m, arg1, arg2)(x, arg1.Value(), arg2.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object>>(x, m, arg1, arg2, arg3)(x, arg1.Value(), arg2.Value(), arg3.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4, object arg5)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4, arg5)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value(), arg5.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4, arg5, arg6)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value(), arg5.Value(), arg6.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4, arg5, arg6, arg7)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value(), arg5.Value(), arg6.Value(), arg7.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7, object arg8)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value(), arg5.Value(), arg6.Value(), arg7.Value(), arg8.Value()));
        }

        public static object Invoke(this object self, string chain, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7, object arg8, object arg9)
        {
            return ExecuteChain(self, chain, (x, m) => GetInvoker<Func<object, object, object, object, object, object, object, object, object, object, object>>(x, m, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)(x, arg1.Value(), arg2.Value(), arg3.Value(), arg4.Value(), arg5.Value(), arg6.Value(), arg7.Value(), arg8.Value(), arg9.Value()));
        }

        private static TDelegate GetInvoker<TDelegate>(object self, string method, params object[] args)
        {
            return GetOrPut(self.GetType(), method, () => Builder.Invoker<TDelegate>(self.GetType(), method, args.Select(ObtainType).ToArray()));
        }

        private static Type ObtainType(object o)
        {
            return o is NullValue ? ((NullValue) o).Type : o.GetType();
        }

        public static T Invoke<T>(this object self, string method)
        {
            return (T)self.Invoke(method);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1)
        {
            return (T)self.Invoke(method,arg1);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2)
        {
            return (T)self.Invoke(method, arg1, arg2);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3,arg4);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4, object arg5)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3, arg4, arg5);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3, arg4, arg5,arg6);
        }
        
        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3, arg4, arg5,arg6,arg7);
        }

        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7, object arg8)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3, arg4, arg5,arg6,arg7,arg8);
        }

        public static T Invoke<T>(this object self, string method, object arg1, object arg2, object arg3, object arg4, object arg5, object arg6, object arg7, object arg8, object arg9)
        {
            return (T)self.Invoke(method, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
        }
        
        public static T Get<T>(this object self, string fieldOrProperty)
        {
            return (T) self.Get(fieldOrProperty);
        }

        public static void Set(this object self, string chain, object value)
        {
            ExecuteChain(self, chain, (x, p) => GetOrPut(x.GetType(), p, () => Builder.FieldOrPropertySetter(x.GetType(), p))(x, value));
        }

        public static bool HasProperty(this object self, string property)
        {
            return self.GetType().GetProperty(property, ReflectionBuilder.InstanceBindingFlags) != null;
        }
        
        public static bool HasField(this object self, string property)
        {
            return self.GetType().GetField(property, ReflectionBuilder.InstanceBindingFlags) != null;
        }

        public static bool HasFieldOrProperty(this object self, string fieldOrProperty)
        {
            return self.HasField(fieldOrProperty) || self.HasProperty(fieldOrProperty);
        }

        private static TDelegate GetOrPut<TDelegate>(Type type, string member, Func<TDelegate> get)
        {
            var key = Key(type, member);
            TDelegate value;

            if (Cache.TryGetValue(key, out value))
                return value;

            value = get();
            Cache.Add(key, value);
            return value;
        }
    }
}